Cấu hình Search Plugin
10. Quy trình thêm search cho một bảng mới
Bước 1: Xác định repo và file cần sửa
Backend search plugin đang nằm trong repo:
k12-tunnel-v5
Khi thêm search cho bảng mới, sửa đúng file config theo module:
| Module GraphQL | Service name | File config |
|---|---|---|
| Student services, thư viện, y tế, nội trú, bus, food | core_student | src/plugins/search-plugin/configs/library-search-configs.js hoặc tạo file config riêng nếu module mới đủ lớn |
| Academic, LMS | core_academic | src/plugins/search-plugin/configs/lms-search-configs.js |
| User/profile dùng trong service student | core_student | src/plugins/search-plugin/configs/user-search-configs.js |
Thư mục chứa các file config search:
src/plugins/search-plugin/configs/
File gom toàn bộ config vào PostGraphile plugin:
src/plugins/search-plugin/create-plugin.js
Nếu tạo thêm file config mới, phải import và spread file đó trong SEARCH_PLUGIN_CONFIGS của create-plugin.js.
File cơ chế sinh SQL search:
src/plugins/search-plugin/search-plugin-fatory.js
Không sửa file này khi chỉ thêm search cho bảng mới. Chỉ sửa khi cần đổi behavior chung của search plugin.
Nếu app đang chạy bằng schema đã export, file generated nằm ở:
src/generated/<module>_schema.mjs
Sau khi sửa config backend, cần export lại schema cho đúng module để file generated nhận field search mới.
Bước 2: Xác định đúng service, schema và table
Không suy đoán từ tên GraphQL. Dùng tên vật lý trong PostgreSQL:
{
serviceName: "core_student",
schemaName: "public",
tableName: "library_example",
}
Quy tắc:
serviceNamephải khớp module trongsrc/modules.config.mjs.schemaNamemặc định làpublic; chỉ bỏ qua khi bảng thật sự nằm trongpublic.tableNamelà tên bảng PostgreSQL dạngsnake_case, không phải tên GraphQL collection.- Bảng muốn search phải được expose trong module tương ứng của
src/modules.config.mjs; nếu bảng bị filter khỏi module thì config search không có tác dụng với schema đó.
Ví dụ module hiện có:
core_student -> /graphql/core-student
core_academic -> /graphql/core-academic
core_shared -> /graphql/core-shared
Bước 3: Chọn cột search trên bảng chính
Chỉ thêm các cột text mà người dùng thật sự cần tìm:
columnNames: ["code", "name", "description"];
Không dùng tên GraphQL camelCase:
// Sai
columnNames: ["fullName"];
// Đúng
columnNames: ["full_name"];
Có thể để rỗng nếu bảng chính không search trực tiếp và chỉ search qua relation:
columnNames: [];
Nếu không khai báo columnNames, plugin mặc định dùng:
columnNames: ["name"];
Chỉ bỏ qua columnNames khi chắc chắn bảng có cột name.
Bước 4: Khai báo relationSearches nếu cần search qua bảng liên quan
Với mỗi relation, xác định rõ:
- Bảng cha hiện tại.
- Bảng liên quan.
- Schema của bảng liên quan nếu khác schema cha.
- Cột join ở bảng cha.
- Cột join tương ứng ở bảng liên quan.
- Có cần
school_idhoặc khóa tenant khác hay không.
Nên viết điều kiện SQL trước:
parent.foreign_id = relation.id
AND parent.school_id = relation.school_id
Sau đó chuyển thành config:
relationSearches: [
{
tableName: "library_subject",
localColumns: ["subject_id", "school_id"],
remoteColumns: ["id", "school_id"],
columnNames: ["name"],
},
];
Với hệ thống nhiều trường, không bỏ school_id nếu relation phụ thuộc tenant. Join thiếu tenant có thể trả dữ liệu sai trường.
Relation khác schema phải khai báo schemaName trên relation:
{
schemaName: "library",
serviceName: "core_student",
tableName: "library_borrow_book_report",
columnNames: [],
relationSearches: [
{
schemaName: "public",
tableName: "library_book",
localColumns: ["book_id", "school_id"],
remoteColumns: ["id", "school_id"],
columnNames: ["title"],
},
],
}
Relation lồng nhau được tính từ bảng cha trực tiếp, không tính từ bảng gốc:
{
serviceName: "core_student",
tableName: "library_dang_ki_ca_biet",
columnNames: ["code"],
relationSearches: [
{
tableName: "library_book_copy",
localColumns: ["book_copy_id", "school_id"],
remoteColumns: ["id", "school_id"],
columnNames: ["isbn"],
relationSearches: [
{
tableName: "library_book",
localColumns: ["book_id", "school_id"],
remoteColumns: ["id", "school_id"],
columnNames: ["title"],
},
],
},
],
}
Bước 5: Thêm config vào đúng file hoặc tạo file config mới
Ví dụ thêm search cho bảng thuộc core_student:
// src/plugins/search-plugin/configs/library-search-configs.js
const LIBRARY_SEARCH_PLUGIN_CONFIGS = [
{
serviceName: "core_student",
tableName: "library_example",
columnNames: ["code", "name"],
relationSearches: [
{
tableName: "library_subject",
localColumns: ["subject_id", "school_id"],
remoteColumns: ["id", "school_id"],
columnNames: ["name"],
},
],
},
];
Nếu module đã có file config phù hợp, thêm object mới vào mảng config hiện có. Ví dụ:
- Bảng thư viện hoặc student service: thêm vào
src/plugins/search-plugin/configs/library-search-configs.js. - Bảng LMS/academic: thêm vào
src/plugins/search-plugin/configs/lms-search-configs.js. - Bảng user/profile đang phục vụ service student: thêm vào
src/plugins/search-plugin/configs/user-search-configs.js.
Nếu module mới đủ lớn hoặc không thuộc các nhóm trên, tạo file config mới trong:
src/plugins/search-plugin/configs/
Đặt tên theo module/nghiệp vụ:
<module>-search-configs.js
Ví dụ tạo file:
src/plugins/search-plugin/configs/medical-search-configs.js
Nội dung file mới phải export default một mảng config:
const MEDICAL_SEARCH_PLUGIN_CONFIGS = [
{
serviceName: "core_student",
tableName: "medical_student_health_records",
columnNames: ["code", "full_name"],
},
];
export default MEDICAL_SEARCH_PLUGIN_CONFIGS;
Sau đó đăng ký file mới trong:
src/plugins/search-plugin/create-plugin.js
Ví dụ:
import MEDICAL_SEARCH_PLUGIN_CONFIGS from "./configs/medical-search-configs.js";
const SEARCH_PLUGIN_CONFIGS = [
...LIBRARY_SEARCH_PLUGIN_CONFIGS,
...LMS_SEARCH_PLUGIN_CONFIGS,
...USER_SEARCH_PLUGIN_CONFIGS,
...MEDICAL_SEARCH_PLUGIN_CONFIGS,
];
Nếu quên import và spread vào SEARCH_PLUGIN_CONFIGS, file config mới sẽ không được PostGraphile nạp và GraphQL condition sẽ không có field search cho bảng đó.
Checklist config bắt buộc:
serviceNameđúng module GraphQL.schemaNameđúng schema database nếu không phảipublic.tableNameđúng tên bảng PostgreSQL.columnNamesdùng tên cột PostgreSQL.- Mỗi relation có
tableName. localColumns.length === remoteColumns.length.- Thứ tự
localColumnsvàremoteColumnskhớp nhau. - Relation có
columnNameshoặcrelationSearchescon. - Relation khác schema đã khai báo
schemaName.
Bước 6: Thêm migration index cho cột search
Search plugin dùng ILIKE, pg_trgm và public.immutable_unaccent. Khi thêm cột search mới, cần thêm migration tạo index phù hợp trong repo migration/database của hệ thống đang quản lý schema đó.
Index nên tạo trên chính bảng chứa cột được search. Nếu cột nằm ở relation, tạo index trên bảng relation, không tạo trên bảng chính.
Pattern index cho search không phân biệt dấu:
CREATE INDEX IF NOT EXISTS <table_name>_<column_name>_trgm_idx
ON <schema_name>.<table_name>
USING gin (
public.immutable_unaccent(coalesce(<column_name>, '')) gin_trgm_ops
);
Ví dụ:
CREATE INDEX IF NOT EXISTS library_book_title_trgm_idx
ON public.library_book
USING gin (
public.immutable_unaccent(coalesce(title, '')) gin_trgm_ops
);
Database cũng phải có:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS unaccent;
và function:
public.immutable_unaccent(text)
Nếu thiếu function này, query có condition.search sẽ lỗi khi PostgreSQL thực thi SQL.
Bước 7: Export lại schema hoặc restart Graphile
Nếu chạy dev trực tiếp bằng config source, restart/reload tiến trình Graphile để plugin được nạp lại.
Nếu chạy qua src/server-multi.mjs, runtime đọc schema đã export trong:
src/generated/<module>_schema.mjs
Khi đó cần export lại đúng module sau khi sửa config. Ví dụ với core_student:
TARGET_MODULE=core_student yarn export
Lệnh export cần DATABASE_URL trỏ đúng database. Không sửa tay file generated trừ khi chỉ kiểm tra tạm thời; nguồn đúng vẫn là config trong src/plugins/search-plugin/configs/*.js.
Bước 8: Cập nhật dashboard/client query
Search phải đi qua condition.search, không đưa keyword vào filter.includesInsensitive nếu bảng đã dùng plugin này.
Ví dụ query search cho PHXTableV6:
export const searchExampleV6 = (params: Params) => ({
enable: true,
keyResult: "allCoreStudentPublicLibraryExamples",
query: `
query searchExampleV6 {
allCoreStudentPublicLibraryExamples(
condition: {
schoolId: ${params.schoolId}
search: "%@value%"
}
filter: {
deletedAt: {isNull: true}
}
first: ${ROW_NUMBER.DEFAULT}
last: ${ROW_NUMBER.DEFAULT}
after: ""
before: ""
) {
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
nodes {
rowId
name
}
totalCount
}
}
`,
});
Khi gắn vào table:
<PHXTableV6
list={listExampleV6(params)}
search={searchExampleV6(params)}
{...otherProps}
/>
Yêu cầu với query dashboard:
- Có
enable: truetrong object search. keyResultđúng root collection.- Dùng placeholder
search: "%@value%"theo convention hiện tại củaPHXTableV6. - Giữ các filter nghiệp vụ như
schoolId,warehouseId,deletedAt, trạng thái, khoảng ngày. - Query list và search trả về cùng shape:
pageInfo,nodes,totalCountvà cùng field trongnodes.
Bước 9: Kiểm tra GraphQL condition
Introspect condition type để chắc field search đã xuất hiện:
query CheckSearchCondition {
__type(name: "CoreStudentPublicLibraryExampleCondition") {
inputFields {
name
}
}
}
Kết quả cần có:
{
"name": "search"
}
Nếu chưa có search, kiểm tra lại:
- Đã sửa đúng config file chưa.
serviceName,schemaName,tableNamecó đúng không.- Bảng có nằm trong module của
src/modules.config.mjskhông. - Đã restart Graphile hoặc export lại schema generated chưa.
- Client có gọi đúng endpoint module không.
Bước 10: Kiểm tra hành vi search
Tối thiểu kiểm tra các trường hợp sau:
- Keyword khớp cột trực tiếp.
- Keyword khớp cột relation.
- Keyword khớp relation lồng nhau nếu có.
- Keyword không dấu khớp dữ liệu có dấu, ví dụ
nguoikhớpngười. - Keyword có dấu nhưng khác dấu vẫn được normalize, ví dụ
ngươikhớpngườitheo cơ chế search không phân biệt dấu hiện tại. - Keyword khác hoa/thường vẫn khớp.
- Keyword nhiều từ chỉ khớp khi cùng một cột hoặc cùng một nhánh relation chứa đủ các từ.
- Keyword không tồn tại trả về
totalCount: 0. - Filter nghiệp vụ vẫn đúng, không trả dữ liệu sai trường, sai kho hoặc bản ghi đã xóa.
- Pagination vẫn hoạt động sau khi search.
Logic search hiện tại:
- Tách keyword bằng khoảng trắng.
- Bỏ dấu cả keyword và dữ liệu trước khi so sánh.
- Dùng
ILIKEđể không phân biệt hoa/thường. - Các cột và relation nối với nhau bằng
OR. - Trong một cột hoặc một nhánh relation, mọi từ trong keyword phải cùng khớp.
- Fuzzy matching dùng
pg_trgmvà giới hạn số từ được sửa bằng cấu hình trongcreateTextSearchPlugin.
Bước 11: Checklist trước khi merge
- Đã thêm config vào đúng file trong
src/plugins/search-plugin/configs/. - Nếu tạo config file mới, đã import trong
src/plugins/search-plugin/create-plugin.js. - Bảng thuộc đúng module trong
src/modules.config.mjs. - Đã thêm migration index cho từng cột search mới.
- Đã export lại
src/generated/<module>_schema.mjsnếu runtime dùng schema generated. - Dashboard query dùng
condition.search. -
PHXTableV6có objectsearchvớienable: true. - List và search query có cùng shape dữ liệu.
- Đã kiểm tra search có dấu, không dấu, nhiều từ và relation.